/**
 * 
 */
package qy.jalgotrade.stratanalyzer;

import java.time.Duration;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.math3.stat.StatUtils;
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;

import com.google.common.math.DoubleMath;

import qy.jalgotrade.event.EventHandler;
import qy.jalgotrade.stratanalyzer.returns.ReturnsAnalyzerBase;
import qy.jalgotrade.strategy.BaseStrategy;
import qy.jalgotrade.utils.CommonUtils;

/**
 * A :class:`pyalgotrade.stratanalyzer.StrategyAnalyzer` that calculates Sharpe ratio for the whole
 * portfolio.
 * 
 * @author c-geo
 *
 */
public class SharpeRatio extends StrategyAnalyzer implements EventHandler {

	public static final int TRADE_DAYS_PER_YEAR_CN = 243;

	public static final int TRADE_DAYS_PER_YEAR_US = 252;

	private boolean __useDailyReturns;

	private List<Double> __returns;

	// Only use when self.__useDailyReturns == False
	private ZonedDateTime __firstDateTime;

	private ZonedDateTime __lastDateTime;

	// Only use when self.__useDailyReturns == True
	private LocalDate __currentDate;

	private int __tradeDaysPerYear;

	/**
	 * 
	 */
	public SharpeRatio() {

		this(true, TRADE_DAYS_PER_YEAR_CN);
	}

	/**
	 * 
	 * @param useDailyReturns True if daily returns should be used instead of the returns for each bar.
	 */
	public SharpeRatio(boolean useDailyReturns) {

		this(useDailyReturns, TRADE_DAYS_PER_YEAR_CN);
	}

	/**
	 * 
	 * @param useDailyReturns  True if daily returns should be used instead of the returns for each bar.
	 * @param tradeDaysPerYear Trading days per year for calculation.
	 */
	public SharpeRatio(boolean useDailyReturns, int tradeDaysPerYear) {

		super();
		__useDailyReturns = useDailyReturns;
		__returns = new ArrayList<>();

		__firstDateTime = null;
		__lastDateTime = null;
		__currentDate = null;
		__tradeDaysPerYear = tradeDaysPerYear;
	}

	public List<Double> getReturns() {

		return __returns;
	}

	/* (non-Javadoc)
	 * @see qy.jalgotrade.event.EventHandler#handle(java.lang.String, java.util.Map)
	 */
	@Override
	public void handle(String eventName, Map<String, Object> params) throws Exception {

		ZonedDateTime dateTime = (ZonedDateTime) params.get("dateTime");
		ReturnsAnalyzerBase returnsAnalyzerBase = (ReturnsAnalyzerBase) params.get("returnsAnalyzerBase");
		__onReturns(dateTime, returnsAnalyzerBase);
	}

	private void __onReturns(ZonedDateTime dateTime, ReturnsAnalyzerBase returnsAnalyzerBase) {

		double netReturn = returnsAnalyzerBase.getNetReturn();
		if (__useDailyReturns) {
			// Calculate daily returns.
			if (dateTime.toLocalDate().equals(__currentDate)) {
				__returns.set(__returns.size() - 1, (1 + __returns.get(__returns.size() - 1)) * (1 + netReturn) - 1);
			} else {
				__currentDate = dateTime.toLocalDate();
				__returns.add(netReturn);
			}
		} else {
			__returns.add(netReturn);
			if (__firstDateTime == null) {
				__firstDateTime = dateTime;
			}
			__lastDateTime = dateTime;
		}
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.stratanalyzer.StrategyAnalyzer#beforeAttach(qy.jalgotrade.strategy.BaseStrategy)
	 */
	@Override
	public void beforeAttach(BaseStrategy strat) throws Exception {

		// Get or create a shared ReturnsAnalyzerBase
		ReturnsAnalyzerBase analyzer = (ReturnsAnalyzerBase) ReturnsAnalyzerBase.getOrCreateShared(strat);
		analyzer.getEvent().subscribe(this);
	}

	/**
	 * Returns the Sharpe ratio for the strategy execution. If the volatility is 0, 0 is returned.
	 * 
	 * @param riskFreeRate The risk free rate per annum.
	 * @return
	 */
	public double getSharpeRatio(double riskFreeRate) {

		return getSharpeRatio(riskFreeRate, true);
	}

	/**
	 * Returns the Sharpe ratio for the strategy execution. If the volatility is 0, 0 is returned.
	 * 
	 * @param riskFreeRate The risk free rate per annum.
	 * @param annualized   True if the sharpe ratio should be annualized.
	 * @return
	 */
	public double getSharpeRatio(double riskFreeRate, boolean annualized) {

		double ret;
		if (__useDailyReturns) {
			ret = sharpeRatio(__returns, riskFreeRate, __tradeDaysPerYear, annualized);
		} else {
			ret = sharpeRatio2(__returns, riskFreeRate, __firstDateTime, __lastDateTime, annualized);
		}

		return ret;
	}

	/**
	 * <pre>
	 * * If using daily bars, tradingPeriods should be set to 252.
	 * * If using hourly bars (with 6.5 trading hours a day) then tradingPeriods should be set to 252 * 6.5 = 1638.
	 * </pre>
	 * 
	 * @param returns        The returns.
	 * @param riskFreeRate   The risk free rate per annum.
	 * @param tradingPeriods The number of trading periods per annum.
	 * @param annualized     True if the sharpe ratio should be annualized.
	 * @return
	 */
	public double sharpeRatio(List<Double> returns, double riskFreeRate, int tradingPeriods, boolean annualized) {

		double ret = 0.0;
		/*
		 * From http://en.wikipedia.org/wiki/Sharpe_ratio: if Rf is a constant risk-free return throughout the period,
		 * then stddev(R - Rf) = stddev(R).
		 */
		// TODO: to optimize
		StandardDeviation stdDev = new StandardDeviation(true);
		Double[] values = new Double[returns.size()];
		double volatility = stdDev.evaluate(ArrayUtils.toPrimitive(returns.toArray(values)));

		if (!DoubleMath.fuzzyEquals(volatility, 0.0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) {
			double rfPerReturn = riskFreeRate / tradingPeriods;
			double avgExcessReturns = StatUtils.mean(ArrayUtils.toPrimitive(returns.toArray(values))) - rfPerReturn;
			ret = avgExcessReturns / volatility;

			if (annualized) {
				ret = ret * Math.sqrt(tradingPeriods);
			}
		}

		return ret;
	}

	/**
	 * 
	 * @param returns       The returns.
	 * @param riskFreeRate  The risk free rate per annum.
	 * @param firstDateTime The first datetime in the period.
	 * @param lastDateTime  The last datetime in the period.
	 * @param annualized    True if the sharpe ratio should be annualized.
	 * @return
	 */
	public double sharpeRatio2(List<Double> returns, double riskFreeRate, ZonedDateTime firstDateTime,
	        ZonedDateTime lastDateTime, boolean annualized) {

		double ret = 0.0;
		/*
		 * From http://en.wikipedia.org/wiki/Sharpe_ratio:
		 * if Rf is a constant risk-free return throughout the period, then stddev(R - Rf) = stddev(R).
		 */
		// TODO: to optimize
		StandardDeviation stdDev = new StandardDeviation(true);
		Double[] values = new Double[returns.size()];
		double volatility = stdDev.evaluate(ArrayUtils.toPrimitive(returns.toArray(values)));

		if (!DoubleMath.fuzzyEquals(volatility, 0.0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) {
			// We use 365 instead of 252 becuase we wan't the diff from 1/1/xxxx to 12/31/xxxx to be 1 year.
			double yearsTraded = daysTraded(firstDateTime, lastDateTime) / 365.0;

			double riskFreeRateForPeriod = riskFreeRate * yearsTraded;
			double rfPerReturn = riskFreeRateForPeriod / returns.size();

			double avgExcessReturns = StatUtils.mean(ArrayUtils.toPrimitive(returns.toArray(values))) - rfPerReturn;
			ret = avgExcessReturns / volatility;
			if (annualized) {
				ret = ret * Math.sqrt(returns.size() / yearsTraded);
			}
		}

		return ret;
	}

	public int daysTraded(ZonedDateTime firstDateTime, ZonedDateTime lastDateTime) {

		Duration delta = Duration.between(firstDateTime, lastDateTime);
		// TODO: long to int
		return Long.valueOf(delta.toDays() + 1).intValue();
	}
}
